Skip to content

fix(extractors): attribute same-file ES6 getter/setter property reads as call edges#2031

Open
carlos-alm wants to merge 1 commit into
fix/issue-1892-constructor-calls-new-x-resolve-to-the-classfrom
fix/issue-1893-es6-getter-setter-property-reads-are-never
Open

fix(extractors): attribute same-file ES6 getter/setter property reads as call edges#2031
carlos-alm wants to merge 1 commit into
fix/issue-1892-constructor-calls-new-x-resolve-to-the-classfrom
fix/issue-1893-es6-getter-setter-property-reads-are-never

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

A bare (non-call) property read or write on an ES6 get/set class accessor (obj.isReady, no call parens) invokes the accessor just as surely as obj.isReady() would if written explicitly — but call-site extraction only ever recognized member_expression nodes used as a call_expression's callee, so accessor reads/writes never produced a calls edge in either engine. Every getter/setter in a codebase (regardless of real usage) showed totalDependents: 0 / role dead/dead-unresolved via codegraph fn-impact/codegraph roles --role dead.

Root cause

Confirmed via codegraph fn-impact on two same-file getters (NativeOrchestrationSession.isReady, SqliteRepository.db) — both showed zero callers despite real usage, ruling out a proximity/confidence issue: this was a pure extraction-coverage gap, not a resolution gap.

Fix

Scoped to the same-file case:

  • this.prop inside one of the accessor's own class's methods
  • varName.prop where varName's type (from the file's own typeMap) is a class also declared in the same file

A same-file accessor registry (collectLocalAccessors/collect_local_accessors, built once per file from the file's own get/set method_definition nodes) gates which bare property reads become synthetic Call entries. Once gated, those entries are indistinguishable from a real this.prop()/varName.prop() call and flow through the existing, unchanged call-resolution cascade in both engines — no new edge kind, no DB schema change, no new serialization across the WASM worker or native FFI boundary.

  • A plain assignment (obj.prop = value) invokes the setter; every other bare usage (reads, compound-assignment targets) invokes the getter.
  • A property declaring both a getter and setter is skipped entirely — the two accessors share a qualified name and resolution has no way to tell them apart, so this mirrors the existing "ambiguous → drop rather than fan out" precedent in resolver/strategy.ts (resolveExactGlobalMatch) rather than risk an edge to the wrong accessor.
  • Mirrored in both engines: src/extractors/javascript.ts (shared by both the WASM query-path and walk-path extraction, via runCollectorWalk) and crates/codegraph-core/src/extractors/javascript.rs (a new walk pass added after type_map is populated, mirroring the existing match_js_call_assignments ordering comment).

Scope boundary

Cross-file accessor recognition (the accessor's class declared in a different file than the read site — the SqliteRepository.db repro in #1893) needs a global accessor-kind registry and a DB schema change (to disambiguate get vs. set at resolution time across files), which is a larger, separate change. Filed as a follow-up: #2030.

Test plan

  • npx vitest run tests/parsers/javascript.test.ts — 8 new unit tests (this-read, cross-var-read via typeMap, setter-write, ambiguous get+set skip, no duplicate on real calls, no false positive on plain method references, static accessor) — 246 passed
  • cargo test --lib extractors::javascript::tests — 7 mirrored Rust unit tests — 175 passed
  • New integration test tests/integration/issue-1893-getter-setter-property-read.test.ts: full-build WASM/native engine parity, incremental-vs-full-build parity, and the ambiguous get+set skip — all confirmed byte-identical across both engines (verified manually against the native addon rebuilt from this branch: 16 nodes / 21 edges on both engines and after an incremental rebuild)
  • npm test — full suite green (3955 passed, 30 skipped, 2 todo, 0 failed)
  • npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts — 206 passed, no precision/recall regression across all 34+ language fixtures
  • cargo test --lib — 584 passed
  • npm run lint — clean (only a pre-existing, unrelated fixture warning)
  • Exact repro confirmed fixed on both engines: fn-impact "Session.isReady" and fn-impact "Repo.db" now show 1 real dependent each (role core), matching between --engine wasm and --engine native

Stacked on #1892 (base branch fix/issue-1892-constructor-calls-new-x-resolve-to-the-class) — only the javascript.ts/javascript.rs/test diff is this issue's change.

… as call edges

A bare (non-call) property read or write on an ES6 get/set class accessor
(`obj.isReady`, no call parens) invokes the accessor just as surely as
`obj.isReady()` would — but call-site extraction only ever recognized
member_expression nodes used as a call_expression's callee, so accessor
reads/writes never produced a `calls` edge in either engine.

Scoped to the same-file case: `this.prop` inside one of the accessor's own
class's methods, or `varName.prop` where `varName`'s type (from the file's
own typeMap) is a class also declared in the same file. A same-file accessor
registry gates which bare property reads become synthetic Call entries, so
they flow through the existing (unchanged) call-resolution cascade in both
engines with no new edge kind, DB schema change, or serialization wiring.
A property declaring both a getter and a setter is skipped (the two
accessors share a qualified name and resolution can't tell them apart).

Cross-file accessor recognition (the accessor's class declared in a
different file than the read site) is filed as a follow-up: #2030.

Fixes #1893

Impact: 12 functions changed, 11 affected
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a coverage gap in call-edge extraction: bare property reads/writes on ES6 get/set class accessors (obj.isReady, no call parens) now produce calls edges, preventing every accessor in a codebase from showing up as dead in impact analysis. The fix is scoped to the same-file case and is mirrored across both the TypeScript (WASM) and Rust (native) engines.

  • Adds collectLocalAccessors (TS + Rust) as a pre-scan pass that builds a ClassName.propName → {get, set} registry, gating which bare member expressions become synthetic Call entries.
  • Accessor reads (this.prop, varName.prop) are routed to the existing call-resolution cascade with no new edge kind or DB schema changes; properties with both a getter and setter are intentionally skipped to avoid ambiguous edges.
  • Eight new unit tests (TS) and seven mirrored Rust tests cover the core cases; a new integration test validates full-build/incremental parity on both engines.

Confidence Score: 4/5

Safe to merge. The change is additive (new synthetic call edges only), touches no existing resolution logic, and is well-tested across both engines.

The break guard child === nameNode in getMethodAccessorKind is dead code on the WASM path due to object-identity semantics. It causes no wrong results today because the grammar never places a get/set token after the method name, but it is inconsistent with the .id-based pattern established elsewhere in this same PR and would silently misbehave if the assumption about grammar structure ever changed.

src/extractors/javascript.ts — the getMethodAccessorKind break guard uses === instead of .id-based comparison.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Adds same-file accessor registry + collector for bare getter/setter property reads. Break guard in getMethodAccessorKind uses reference equality (always false on WASM); otherwise logic is correct and mirrors the Rust implementation.
crates/codegraph-core/src/extractors/javascript.rs Rust mirror of the TS accessor-read attribution. Correctly uses node.id() for identity comparison. Logic is well-structured and consistent with existing call-assignment patterns.
tests/integration/issue-1893-getter-setter-property-read.test.ts New integration test covering full-build, incremental-rebuild parity, and both WASM/native engines for getter-read, setter-write, and ambiguous get+set skip scenarios.
tests/parsers/javascript.test.ts Eight new unit tests covering this-read, typeMap-based var-read, setter-write, ambiguous skip, no-duplicate on real call, no false positive for plain method reference, and static accessor.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[AST walk: member_expression node] --> B{parent is call_expression\nwith this as callee?}
    B -->|yes| C[Skip — already handled\nby regular call path]
    B -->|no| D{obj type?}
    D -->|this| E[findParentClass → className]
    D -->|identifier| F[typeMap lookup → className]
    D -->|other| G[Skip]
    E --> H{className.propName\nin localAccessors?}
    F --> H
    H -->|not found| I[Skip — not a same-file accessor]
    H -->|found, both get+set| J[Skip — ambiguous target]
    H -->|found, unambiguous| K{parent is plain\nassignment_expression?}
    K -->|yes → setter| L{accessor has set?}
    K -->|no → getter| M{accessor has get?}
    L -->|no| N[Skip]
    L -->|yes| O[Emit synthetic Call edge]
    M -->|no| N
    M -->|yes| O
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[AST walk: member_expression node] --> B{parent is call_expression\nwith this as callee?}
    B -->|yes| C[Skip — already handled\nby regular call path]
    B -->|no| D{obj type?}
    D -->|this| E[findParentClass → className]
    D -->|identifier| F[typeMap lookup → className]
    D -->|other| G[Skip]
    E --> H{className.propName\nin localAccessors?}
    F --> H
    H -->|not found| I[Skip — not a same-file accessor]
    H -->|found, both get+set| J[Skip — ambiguous target]
    H -->|found, unambiguous| K{parent is plain\nassignment_expression?}
    K -->|yes → setter| L{accessor has set?}
    K -->|no → getter| M{accessor has get?}
    L -->|no| N[Skip]
    L -->|yes| O[Emit synthetic Call edge]
    M -->|no| N
    M -->|yes| O
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(extractors): attribute same-file ES6..." | Re-trigger Greptile

const nameNode = methNode.childForFieldName('name');
for (let i = 0; i < methNode.childCount; i++) {
const child = methNode.child(i);
if (!child || child === nameNode) break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The break guard child === nameNode uses object reference equality, which is always false in the tree-sitter WASM binding — as noted in the comment on collectAccessorPropertyRead in the very same PR: "tree-sitter (WASM) mints a fresh wrapper object on every childForFieldName()/parent access, so === … is always false." The Rust mirror correctly uses child.id() == name_node.id(). Currently harmless (the tree-sitter grammar places all get/set modifiers strictly before the name node, so no false-positive is possible), but the break is dead code on the WASM path and inconsistent with the .id-based comparisons elsewhere in this PR.

Suggested change
if (!child || child === nameNode) break;
if (!child || child.id === nameNode?.id) break;

Fix in Claude Code

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

12 functions changed11 callers affected across 1 files

  • extractSymbolsQuery in src/extractors/javascript.ts:395 (1 transitive callers)
  • LocalAccessorInfo.get in src/extractors/javascript.ts:596 (0 transitive callers)
  • LocalAccessorInfo.set in src/extractors/javascript.ts:597 (0 transitive callers)
  • getMethodAccessorKind in src/extractors/javascript.ts:609 (14 transitive callers)
  • collectLocalAccessors in src/extractors/javascript.ts:627 (3 transitive callers)
  • walk in src/extractors/javascript.ts:629 (14 transitive callers)
  • localTypeMapTypeName in src/extractors/javascript.ts:654 (13 transitive callers)
  • collectAccessorPropertyRead in src/extractors/javascript.ts:672 (14 transitive callers)
  • extractSymbolsWalk in src/extractors/javascript.ts:1039 (1 transitive callers)
  • CollectorWalkTargets.localAccessors in src/extractors/javascript.ts:4208 (0 transitive callers)
  • runCollectorWalk in src/extractors/javascript.ts:4234 (3 transitive callers)
  • walk in src/extractors/javascript.ts:4235 (14 transitive callers)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant